// nth-graph-view jquery plugin
// author: Nate Morse copyright 2009
// BSD license
// version: 0.02
// project site: http://code.google.com/p/nth-graph/
// source repository:  
// http://nth-graph.googlecode.com/svn/trunk/nth-graph/ide/svg_based
// usage: 
//   $("#svgElement").nthGraphView({options});
// options: 
//   g:object nth-graph in JSON or JSOL
//   gv:object nth-graph-view in JSON or JSOL
//   warn:function calls a function when a warning is encoutered default:console
//   debug:function called for debuging

(function($) {

	$.fn.nthGraphView = function(settings) {
		var config = {
				"g":{}, 
				"gv":{
					"defaultNode":{"shape":"circle", "r":25}, 
					"defaultEdge":{"magnitude":30, "arrow":true}
				},  
				"gvEvents":{
					"allNodes":{
						"mouseover":function(e){
							var dom_e = $(e); 
							dom_e.css('fill', "#FF0000");
						}, 
						"click":function(e){
							var dom_e = $(e); 
							dom_e.attr('fill', "#0000FF");
						}
					},
					"allEdges":{},
					"ioNodes":{}
				},  
				"warn": function(msg){alert(msg);},
				"debug": function(msg){}
		};
		var highlightSelf = function(){
			alert("called highlightSelf");
		};
		var showProperties = function(){
			alert("called showProperties");
		};
		var nth = {
			graph:{},
			view:{},
			graphView: {
				omee:null,
				show: function(omee) {
					nth.debug("start show...");
					nth.attachEnv(omee);
					nth.graphView.traverse();
					// bind events
					if (nth.exists(config, "gvEvents", "allNodes")) {
						$.each(config.gvEvents.allNodes, function(ev_name, ev_handler) {
							//alert("bind "+ev_name+" to "+ev_handler);
							$('.nth-node').bind(ev_name, ev_handler);
						});
					}
					
					nth.detachEnv();
				},
				traverse: function() {
					// collect in a fragment. (note the extra 'true' argument is for use in SVG Web)
		      var frag = document.createDocumentFragment(true);
					$.each(nth.graph.edges, function(edge_index, edge) {
						// draw this edge 
						edge.view = $.extend(true, {}, config.gv.defaultEdge, nth.view.edges[edge_index]);
						var n1 = nth.graph.nodes[edge.from];
						n1.view = $.extend(true, {}, config.gv.defaultNode, nth.view.nodes[edge.from]);
						var n2 = nth.graph.nodes[edge.to];
						n2.view = $.extend(true, {}, config.gv.defaultNode, nth.view.nodes[edge.to]);
						//var cp1 = nth.graphView.getNodeConnectPoint(edge.from);
						//var cp2 = nth.graphView.getNodeConnectPoint(edge.to);
						var fromCP = nth.getConnectionPoint("tail", n1, edge);
						var toCP = nth.getConnectionPoint("head", n2, edge);
						var elements = nth.renderEdge(fromCP, toCP, edge);
						if (elements[0]) {
							frag.appendChild(elements[0]);
						}
						if (elements[1]) {
							frag.appendChild(elements[1]);
						}
						if (elements[2]) {
							frag.appendChild(elements[2]);
						}

					});
					$.each(nth.graph.nodes, function(node_index, node) {
						// draw this node 
						node.view = $.extend(true, {}, config.gv.defaultNode, nth.view.nodes[node_index]);
						// maybe this next line does not display anything because of the wrong namespace??
						//$(nth.graphView.omee).append('<circle id="node_'+node_index+'" cx="'+((node_index*25)+20)+'" cy="75" r="10" stroke="black" stroke-width="1" />');
						var el = document.createElementNS(svgns, 'circle');
						var cx = node.view.cx? node.view.cx: node_index*45+30;
						var cy = node.view.cy? node.view.cy: 100;
						var r = node.view.r? node.view.r: 30;
						var fill = node.view.fill? node.view.fill: '#FBF7F1';
						var stroke = node.view.stroke? node.view.stroke: 'black';
						el.setAttribute('class', 'nth-node');
						el.setAttribute('cx', cx);
						el.setAttribute('cy', cy);
						el.setAttribute('r', r);
						el.setAttribute('fill', fill);
						el.setAttribute('stroke-width', '1px');
						el.setAttribute('stroke', stroke);
						frag.appendChild(el);
						var label = document.createElementNS(svgns, 'text');
						label.setAttribute('class', 'nth-node nth-label');
						label.setAttribute('x', cx);
						label.setAttribute('y', cy+5);
						var label_text = node.ele? node.ele: "";
						label_text = node.name? node.name: label_text;
						label_text = label_text==""? "node: "+node_index: label_text;
						$(label).text(label_text);
						frag.appendChild(label);
					});
					nth.graphView.omee.appendChild(frag);
				},
				getNodeConnectPoint: function(node_index) {
					var node_view = nth.view.nodes[node_index];
					var cpx = node_view.cx? node_view.cx: 100;
					var cpy = node_view.cy? node_view.cy: 100;
					return [cpx, cpy];
				}
			},
			warn: function(message) {
				config.warn.call(null, message);
				//alert("warn:"+message);
			},
			debug: function(message) {
				config.debug.call(null, message);
				//alert("debug:"+message);
			},
			
			attachEnv: function(omee) {
				// save the outer most encapsulating element (omee)
				nth.graphView.omee = omee;
			},
			detachEnv: function() {
				// unbind here??
			},
			
			// exists(base_object [, property]...)
			// This function checks if the base object is defined and then
			// goes through all the arguments to see if all the properties exist
			// returns boolean		
			exists: function() {
				var i, obj = arguments[0];
				if(typeof obj == "undefined") {
					return false;
				}
				for(i = 1; i < arguments.length; i += 1) {
					obj = obj[arguments[i]];
					if(typeof obj == "undefined") {
						return false;
					}
				}
				return true;
			},

			// call nth.getConnectionPoint(edge_end=['head'|'tail'], node, edge) {
			getConnectionPoint: function(edge_end, node, edge) {
				var mag = config.gv.defaultEdge.magnitude;
				var angle_in_deg = (edge_end == 'head') ? 180 : 0;
				var angle = 0;
				var proportion = 1;
				var node_bb = nth.getBoundingBox(node);
				var x = node_bb.x;
				var y = node_bb.y;
				var width = node_bb.width;
				var height = node_bb.height;
				var cx = x + (width / 2);
				var cy = y + (height / 2);
				var controlPointXOffset;
				var controlPointYOffset;
				var pointX;
				var pointY;
				if (edge.view && edge.view[edge_end]) {
					angle_in_deg = (nth.exists(edge.view[edge_end], 'angle')) ? edge.view[edge_end].angle : angle_in_deg;
					mag = (nth.exists(edge.view[edge_end], 'mag')) ? edge.view[edge_end].mag : mag;
				}
				var shape = (nth.exists(node.view, 'shape')) ? node.view.shape : config.gv.defaultNode.shape;
				if (shape == 'circle') {
					pointX = cx + Math.cos(angle_in_deg * 0.0174532925) * (width / 2);
					pointY = cy + Math.sin(angle_in_deg * 0.0174532925) * (width / 2);
				}
				else 
					if (shape == 'rect' || shape == 'roundRect') {
						// note: this is based on diagonal quadrants (not strict angle), -- better re-propotioning UI behavior (IMHO). 
						while (angle_in_deg < 0) {
							angle_in_deg += 360;
						}
						angle_in_deg = angle_in_deg % 360; // nomalize
						var lrx = x + width; // lower right x
						var lry = y + height; // lower right y
						var pad = (nth.exists(node.view, 'padding')) ? node.view.padding : config.vg.defaultNode.view.padding;
						if (angle_in_deg >= 45 && angle_in_deg < 135) { //bottom
							pointX = lrx - ((angle_in_deg - 45) / 90) * width;
							pointY = lry;
							if (shape == 'roundRect' && pad > 6) {
								if (pointX < (x + pad)) { // left
									proportion = (pointX - x) / pad;
									angle = 135 - (proportion * 45);
									pointX = x + pad + Math.cos(angle * 0.0174532925) * pad;
									pointY = (lry - pad) + Math.sin(angle * 0.0174532925) * pad;
								}
								else 
									if (pointX > (lrx - pad)) { // right
										proportion = (pointX - lrx) / pad;
										angle = 45 - (proportion * 45);
										pointX = (lrx - pad) + Math.cos(angle * 0.0174532925) * pad;
										pointY = (lry - pad) + Math.sin(angle * 0.0174532925) * pad;
									}
							}
						}
						else 
							if (angle_in_deg >= 135 && angle_in_deg < 225) { //left
								pointX = x;
								pointY = (y + height) - ((angle_in_deg - 135) / 90) * height;
								if (shape == 'roundRect' && pad > 6) {
									if (pointY < (y + pad)) { //  top
										proportion = (pointY - y) / pad;
										angle = 225 - (proportion * 45);
										pointX = x + pad + Math.cos(angle * 0.0174532925) * pad;
										pointY = y + pad + Math.sin(angle * 0.0174532925) * pad;
									}
									else 
										if (pointY > (lry - pad)) { //  bottom
											proportion = (pointY - lry) / pad;
											angle = 135 - (proportion * 45);
											pointX = x + pad + Math.cos(angle * 0.0174532925) * pad;
											pointY = (lry - pad) + Math.sin(angle * 0.0174532925) * pad;
										}
								}
							}
							else 
								if (angle_in_deg >= 225 && angle_in_deg < 315) { //top
									pointX = x + ((angle_in_deg - 225) / 90) * width;
									pointY = y;
									if (shape == 'roundRect' && pad > 6) {
										if (pointX < (x + pad)) { // left
											proportion = (pointX - x) / pad;
											angle = 225 + (proportion * 45);
											pointX = x + pad + Math.cos(angle * 0.0174532925) * pad;
											pointY = y + pad + Math.sin(angle * 0.0174532925) * pad;
										}
										else 
											if (pointX > (lrx - pad)) { // right
												proportion = (pointX - lrx) / pad;
												angle = 315 + (proportion * 45);
												pointX = (lrx - pad) + Math.cos(angle * 0.0174532925) * pad;
												pointY = y + pad + Math.sin(angle * 0.0174532925) * pad;
											}
									}
								}
								else { // right
									pointX = lrx;
									pointY = y + ((angle_in_deg + 45) % 360 / 90) * height;
									if (shape == 'roundRect' && pad > 6) {
										if (pointY < (y + pad)) { //  top
											proportion = (pointY - y) / pad;
											angle = 315 + (proportion * 45);
											pointX = (lrx - pad) + Math.cos(angle * 0.0174532925) * pad;
											pointY = y + pad + Math.sin(angle * 0.0174532925) * pad;
										}
										else 
											if (pointY > (lry - pad)) { //  bottom
												proportion = (pointY - lry) / pad;
												angle = 45 + (proportion * 45);
												pointX = (lrx - pad) + Math.cos(angle * 0.0174532925) * pad;
												pointY = (lry - pad) + Math.sin(angle * 0.0174532925) * pad;
											}
									}
								}
					}
					else {
						pointX = cx;
						pointY = cy;
					}
				
				controlPointXOffset = Math.cos(angle_in_deg * 0.0174532925) * mag;
				controlPointYOffset = Math.sin(angle_in_deg * 0.0174532925) * mag;
				return [{
					x: pointX,
					y: pointY
				}, {
					x: pointX + controlPointXOffset,
					y: pointY + controlPointYOffset
				}];
			},
			
			renderEdge: function (from, to, edge) {
				// TODO implement Knots, edge.knots ...

				var stroke = edge.type=='get'? 'blue': 'green';
				stroke = edge.type=='set'? 'red': stroke;
				var stroke_width = edge.view['stroke-width']? edge.view['stroke-width']: 2;
				var arrow = (nth.exists(edge.view, 'arrow'))? edge.view.arrow: config.gv.defaultEdge.arrow;
				if (arrow) {
					if (to[1].x != to[0].x || to[1].y != to[0].y) {
						var dx = to[0].x - to[1].x;
						var dy = to[0].y - to[1].y;
						// calculate angle and draw the arrow head
						angle = nth.angleFromDelta(dx, dy);
						arrow = document.createElementNS(svgns, 'path');
						d_str = "M-7 -3 L0 0 -7 3 Z";
						arrow.setAttribute('class', 'nth-edge');
						arrow.setAttribute('d', d_str);
						arrow.setAttribute('style', "fill:none;stroke:"+stroke+";stroke-width:"+stroke_width);
						arrow.setAttribute("transform", "translate("+to[0].x+", "+to[0].y+"), rotate("+angle+")");
						to[0].x = to[0].x - Math.cos(angle * 0.0174532925) * 7;
						to[0].y = to[0].y - Math.sin(angle * 0.0174532925) * 7;
						to[1].x = to[1].x - Math.cos(angle * 0.0174532925) * 7;
						to[1].y = to[1].y - Math.sin(angle * 0.0174532925) * 7;
					}
				}
				var el = document.createElementNS(svgns, 'path');
				var d_str = "M"+from[0].x+" "+from[0].y+" C"+from[1].x+" "+from[1].y+" "+to[1].x+" "+to[1].y+" "+to[0].x+" "+to[0].y+" ";
				el.setAttribute('d', d_str);
				el.setAttribute('class', 'nth-edge');
				el.setAttribute('style', "fill:none;stroke:"+stroke+";stroke-width:"+stroke_width);

				var label = document.createElementNS(svgns, 'text');
				label.setAttribute('class', 'nth-edge nth-label');
				label.setAttribute('x', (to[1].x + from[1].x)/2);
				label.setAttribute('y', (to[1].y + from[1].y)/2);
				var label_text = nth.exists(edge, 'ele')? edge.ele: "";
				label_text = nth.exists(edge, 'name')? edge.name: label_text;
				//label_text = label_text==""? "edge: "+edge_index: label_text;
				$(label).text(label_text);

				return [el, arrow, label];
			},
			
			angleFromDelta: function (x, y) {
				if (Math.abs(x) < Math.pow(10, -6)) {
					if (Math.abs(y) < Math.pow(10, -6)) { 
						return 0;
					}
					if (y > 0) { 
						return 90;
					}
					if (y < 0) { 
						return 270;
					}
				}
				if (x > 0) {
					if (y >= 0) { 
						return 180 * Math.atan(y / x) / Math.PI;
					}
					if (y < 0) { 
						return 180 * Math.atan(y / x) / Math.PI - (-1) * 360;
					}
				}
				if (x < 0) {
					if (y >= 0) { 
						return 180 - 180 * Math.atan(-y / x) / Math.PI;
					}
					if (y < 0) { 
						return -180 - (-1) * 180 * Math.atan(y / x) / Math.PI - (-1) * 360;
					}
				}
			},
			getBoundingBox: function (node) {
				var nv = node.view;
				return {"x":nv.cx-nv.r, "y":nv.cy-nv.r, "width":nv.r*2, "height":nv.r*2};
			}
		};
		
		// now start processing this nthGraph instance.
		if (settings) {$.extend(true, config, settings);}
		if (config.g && typeof config.g == 'object') {$.extend(true, nth.graph, config.g);}
		if (config.gv && typeof config.gv == 'object') {$.extend(true, nth.view, config.gv);}
		else if (config.cgref && typeof config.cgref == 'string') {
			nth.loadControlGraph(config.cgref);
		}
		else {nth.warn("No control graph (cg) indicated.");}
		
		// so far this is only tested on one (jQuery selected) element at a time, 
		// but it could be used on an array of elements,
		// Input includes metadata (if the metadata.js lib is included),
		// or input is from form elements. 
		// Output will be applied back to output decendant from the given element. 
		this.each(function() {
			nth.graphView.show(this);
		});
		
		return this;
	};
})(jQuery);
